package com.iwindplus.cloud.i18n.support;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.text.CharSequenceUtil;
import com.alibaba.cloud.nacos.NacosConfigManager;
import com.alibaba.nacos.api.exception.NacosException;
import com.iwindplus.boot.domain.constant.CommonConstant.SymbolConstant;
import java.io.IOException;
import java.io.StringReader;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.locks.ReentrantLock;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.support.AbstractResourceBasedMessageSource;
import org.springframework.util.StringUtils;

/**
 * nacos 国际化.
 *
 * @author zengdegui
 * @since 2025/03/03 21:46
 */
@Slf4j
@EqualsAndHashCode(callSuper = false)
@Data
@NoArgsConstructor
@AllArgsConstructor
public class NacosBundleMessageSource extends AbstractResourceBasedMessageSource {

    /**
     * Nacos配置管理器.
     */
    private NacosConfigManager nacosConfigManager;

    /**
     * Naocs分组.
     */
    private String nacosGroup;

    /**
     * 国际化文件后缀.
     */
    private String fileSuffix;

    /**
     * 是否允许并发刷新.
     */
    private boolean concurrentRefresh;

    /**
     * 缓存文件名.
     */
    private final ConcurrentMap<String, Map<Locale, List<String>>> cachedFileNames = new ConcurrentHashMap<>(16);

    /**
     * 缓存属性.
     */
    private final ConcurrentMap<String, PropertiesHolder> cachedProperties = new ConcurrentHashMap<>(16);

    /**
     * 缓存合并属性.
     */
    private final ConcurrentMap<Locale, PropertiesHolder> cachedMergedProperties = new ConcurrentHashMap<>(16);

    @Override
    protected String resolveCodeWithoutArguments(String code, Locale locale) {
        if (this.getCacheMillis() < 0) {
            PropertiesHolder propHolder = this.getMergedProperties(locale);
            String result = propHolder.getProperty(code);
            if (Objects.nonNull(result)) {
                return result;
            }
        } else {
            for (String basename : this.getBasenameSet()) {
                List<String> fileNames = this.calculateAllFileNames(basename, locale);
                for (String fileName : fileNames) {
                    PropertiesHolder propHolder = this.getProperties(fileName);
                    String result = propHolder.getProperty(code);
                    if (Objects.nonNull(result)) {
                        return result;
                    }
                }
            }
        }
        return null;
    }

    @Override
    protected MessageFormat resolveCode(String code, Locale locale) {
        if (this.getCacheMillis() < 0) {
            PropertiesHolder propHolder = this.getMergedProperties(locale);
            MessageFormat result = propHolder.getMessageFormat(code, locale);
            if (Objects.nonNull(result)) {
                return result;
            }
        } else {
            for (String basename : this.getBasenameSet()) {
                List<String> fileNames = this.calculateAllFileNames(basename, locale);
                for (String fileName : fileNames) {
                    PropertiesHolder propHolder = this.getProperties(fileName);
                    MessageFormat result = propHolder.getMessageFormat(code, locale);
                    if (Objects.nonNull(result)) {
                        return result;
                    }
                }
            }
        }
        return null;
    }

    /**
     * 强制刷新
     *
     * @param fileName 文件名
     * @param config   变更后的配置内容
     */
    public void forceRefresh(String fileName, String config) throws IOException {
        synchronized (this) {
            Properties props = new Properties();
            props.load(new StringReader(config));
            long fileTimestamp = -1;
            PropertiesHolder propHolder = new PropertiesHolder(props, fileTimestamp);
            this.cachedProperties.put(fileName, propHolder);
            this.cachedMergedProperties.clear();
        }
    }

    private PropertiesHolder getMergedProperties(Locale locale) {
        PropertiesHolder mergedHolder = this.cachedMergedProperties.get(locale);
        if (Objects.nonNull(mergedHolder)) {
            return mergedHolder;
        }
        mergedHolder = this.mergeProperties(this.collectPropertiesToMerge(locale));
        PropertiesHolder existing = this.cachedMergedProperties.putIfAbsent(locale, mergedHolder);
        if (Objects.nonNull(existing)) {
            mergedHolder = existing;
        }
        return mergedHolder;
    }

    private PropertiesHolder mergeProperties(List<PropertiesHolder> holders) {
        Properties mergedProps = new Properties();
        long latestTimestamp = -1;
        for (PropertiesHolder holder : holders) {
            mergedProps.putAll(holder.getProperties());
            if (holder.getFileTimestamp() > latestTimestamp) {
                latestTimestamp = holder.getFileTimestamp();
            }
        }
        return new PropertiesHolder(mergedProps, latestTimestamp);
    }

    private List<PropertiesHolder> collectPropertiesToMerge(Locale locale) {
        String[] baseNames = StringUtils.toStringArray(getBasenameSet());
        List<PropertiesHolder> holders = new ArrayList<>(baseNames.length);
        for (int ii = baseNames.length - 1; ii >= 0; ii--) {
            List<String> fileNames = this.calculateAllFileNames(baseNames[ii], locale);
            for (int jj = fileNames.size() - 1; jj >= 0; jj--) {
                String fileName = fileNames.get(jj);
                PropertiesHolder propHolder = this.getProperties(fileName);
                if (Objects.nonNull(propHolder.getProperties())) {
                    holders.add(propHolder);
                }
            }
        }
        return holders;
    }

    private List<String> calculateAllFileNames(String basename, Locale locale) {
        if (CharSequenceUtil.contains(basename, SymbolConstant.SLASH)) {
            basename = CharSequenceUtil.subAfter(basename, SymbolConstant.SLASH, true);
        }
        Map<Locale, List<String>> localeMap = this.cachedFileNames.get(basename);
        if (MapUtil.isNotEmpty(localeMap)) {
            List<String> fileNames = localeMap.get(locale);
            if (CollUtil.isNotEmpty(fileNames)) {
                return fileNames;
            }
        }
        List<String> fileNames = new ArrayList<>(7);
        fileNames.addAll(this.calculateFileNamesForLocale(basename, locale));
        Locale defaultLocale = this.getDefaultLocale();
        if (Objects.nonNull(defaultLocale) && !defaultLocale.equals(locale)) {
            List<String> fallbackFileNames = this.calculateFileNamesForLocale(basename, defaultLocale);
            for (String fallbackFileName : fallbackFileNames) {
                if (!fileNames.contains(fallbackFileName)) {
                    fileNames.add(fallbackFileName);
                }
            }
        }
        fileNames.add(basename);
        if (MapUtil.isEmpty(localeMap)) {
            localeMap = new ConcurrentHashMap<>(16);
            Map<Locale, List<String>> existing = this.cachedFileNames.putIfAbsent(basename, localeMap);
            if (MapUtil.isNotEmpty(existing)) {
                localeMap = existing;
            }
        }
        localeMap.put(locale, fileNames);
        return fileNames;
    }

    private List<String> calculateFileNamesForLocale(String basename, Locale locale) {
        List<String> result = new ArrayList<>(3);
        String language = locale.getLanguage();
        String country = locale.getCountry();
        String variant = locale.getVariant();
        StringBuilder temp = new StringBuilder(basename);
        temp.append(SymbolConstant.UNDERLINE);
        if (language.length() > 0) {
            temp.append(language);
            result.add(0, temp.toString());
        }
        temp.append(SymbolConstant.UNDERLINE);
        if (country.length() > 0) {
            temp.append(country);
            result.add(0, temp.toString());
        }
        final boolean flag = variant.length() > 0 && (language.length() > 0 || country.length() > 0);
        if (flag) {
            temp.append(SymbolConstant.UNDERLINE).append(variant);
            result.add(0, temp.toString());
        }
        return result;
    }

    private PropertiesHolder getProperties(String fileName) {
        PropertiesHolder propHolder = this.cachedProperties.get(fileName);
        long originalTimestamp = -2;
        if (Objects.nonNull(propHolder)) {
            originalTimestamp = propHolder.getRefreshTimestamp();
            if (originalTimestamp == -1 || originalTimestamp > System.currentTimeMillis() - this.getCacheMillis()) {
                return propHolder;
            }
        } else {
            propHolder = new PropertiesHolder();
            PropertiesHolder existingHolder = this.cachedProperties.putIfAbsent(fileName, propHolder);
            if (Objects.nonNull(existingHolder)) {
                propHolder = existingHolder;
            }
        }
        if (this.concurrentRefresh && propHolder.getRefreshTimestamp() >= 0) {
            if (Boolean.FALSE.equals(propHolder.refreshLock.tryLock())) {
                return propHolder;
            }
        }
        propHolder.refreshLock.lock();
        try {
            PropertiesHolder existingHolder = this.cachedProperties.get(fileName);
            if (Objects.nonNull(existingHolder) && existingHolder.getRefreshTimestamp() > originalTimestamp) {
                return existingHolder;
            }
            return this.refreshProperties(fileName);
        } finally {
            propHolder.refreshLock.unlock();
        }
    }

    private PropertiesHolder refreshProperties(String fileName) {
        long fileTimestamp = -1;
        PropertiesHolder propHolder;
        try {
            Properties props = this.loadProperties(fileName);
            propHolder = new PropertiesHolder(props, fileTimestamp);
        } catch (IOException | NacosException ex) {
            log.error("Could not get properties form nacos", ex);
            propHolder = new PropertiesHolder();
        }
        propHolder.setRefreshTimestamp(this.getCacheMillis() < 0 ? -1 : System.currentTimeMillis());
        this.cachedProperties.put(fileName, propHolder);
        return propHolder;
    }

    private Properties loadProperties(String fileName) throws IOException, NacosException {
        Properties props = new Properties();
        final String dataId = new StringBuilder(fileName).append(this.fileSuffix).toString();
        String config = this.nacosConfigManager.getConfigService().getConfig(dataId, this.nacosGroup, 5000);
        if (CharSequenceUtil.isNotBlank(config)) {
            log.info("从Nacos加载国际化配置，文件名={}", dataId);

            props.load(new StringReader(config));
        }
        return props;
    }

    @Data
    protected class PropertiesHolder {

        /**
         * 属性.
         */
        private final Properties properties;

        /**
         * 文件时间戳.
         */
        private final long fileTimestamp;

        /**
         * 刷新时间戳.
         */
        private volatile long refreshTimestamp = -2;

        /**
         * 可重入锁.
         */
        private final ReentrantLock refreshLock = new ReentrantLock();

        private final ConcurrentMap<String, Map<Locale, MessageFormat>> cachedMessageFormats =
            new ConcurrentHashMap<>(16);

        public PropertiesHolder() {
            this.properties = null;
            this.fileTimestamp = -1;
        }

        public PropertiesHolder(Properties properties, long fileTimestamp) {
            this.properties = properties;
            this.fileTimestamp = fileTimestamp;
        }

        public String getProperty(String code) {
            if (Objects.isNull(this.properties)) {
                return null;
            }
            return this.properties.getProperty(code);
        }

        public MessageFormat getMessageFormat(String code, Locale locale) {
            if (Objects.isNull(this.properties)) {
                return null;
            }
            Map<Locale, MessageFormat> localeMap = this.cachedMessageFormats.get(code);
            if (MapUtil.isNotEmpty(localeMap)) {
                final MessageFormat result = localeMap.get(locale);
                if (Objects.nonNull(result)) {
                    return result;
                }
            }
            String msg = this.properties.getProperty(code);
            if (CharSequenceUtil.isBlank(msg)) {
                return null;
            }
            if (MapUtil.isEmpty(localeMap)) {
                localeMap = new ConcurrentHashMap<>(16);
                Map<Locale, MessageFormat> existing = this.cachedMessageFormats.putIfAbsent(code, localeMap);
                if (MapUtil.isNotEmpty(existing)) {
                    localeMap = existing;
                }
            }
            MessageFormat result = createMessageFormat(msg, locale);
            localeMap.put(locale, result);
            return result;
        }
    }
}
